package torchtools.binary;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 *
 * @author Matt
 */
public class TLSave extends RandomAccessFile {
    
    public final static String ENCODING = "UTF-16LE";
    public final static byte DE = (byte)0xDE;
    public final static byte FF = (byte)0xFF;
    public final static byte NIL = (byte)0x00;
    
    /**
     * Roughly searches for the given byte array within the file -- note that 
     * this may not find all hits, as it's tuned for specific finds. 
     * For example:<br>
     * <pre>search for <b>FF FF DE</b> within...
     * 00 11 FF FF DE  => will return the index before the first FF
     * FF FF FF FF DE  => will not find a hit</pre>
     * 
     * Note that the file pointer will be not seek back to the start of the
     * found item, i.e. it will be left at the end of the byte array. However,
     * the return value will be the start of the byte array (or -1 if not found).
     * 
     * @param bArray
     * @return
     * @throws java.io.IOException
     */
    public long quickFind(byte[] bArray) throws IOException {
        if (bArray==null||bArray.length==0) 
            throw new IllegalArgumentException("empty array");
        long start = -1;
        int bInd = 0;
        while (getFilePointer() < length()) {
            byte b = readByte();
            //we found a correct byte!
            if (b == bArray[bInd]) {
                if (bInd==0) { //set starting byte
                    start = getFilePointer()-1;
                } else if (bInd==bArray.length-1) { //we've reached the end
                    return start;
                }
                bInd++;
            } 
            //did not find the right byte, so reset the index
            else if (bInd!=0) {
                bInd = 0;
            }
        }
        return -1;
    }
    
    public static void main(String[] args) throws IOException {
        TLPlayerSave save = new TLPlayerSave("0.svt", "rw");
        
        //seek to the TLString we want to change... in this case player name
        save.seek(save.getPlayerNamePtr());
        
        //This will change the TLString at current file pointer, regardless of 
        //the old string's length
        int success = TLSave.replaceTLString(save, "Bobby");
        if (success==-1) {
            System.out.println("Error: could not replace string.");
        } else if (success==0) {
            System.out.println("Success! No change in length; file still open.");
            save.close();
        } else {
            System.out.println("Success! Length changed; file closed.");
        }
        
        
        /*//open with "rw" so we can read and write
        TLPlayerSave save = new TLPlayerSave("0.svt", "rw");
        System.out.println("Name: "+save.getPlayerName());
        System.out.println("Gold: "+save.getGold()+" gp");
        System.out.println("Difficulty: "+save.getDifficultyText());
        
        //use convenience method writeDifficulty, set to HARD
        save.writeDifficulty(TLPlayerSave.DIFFICULTY_HARD);
        
        //print new difficulty text
        System.out.println("New Difficulty: "+save.getDifficultyText());
        save.close();*/
        /*file.updatePlayerPointers();
        System.out.println("Name: "+file.getPlayerName());
        file.seek(file.getPlayerNamePtr());
        TLSave.replaceTLString(file, "Blah");
                
        //reload
        file = new TLPlayerSave("0.svt", "rw", false);
        file.updatePlayerPointers();
        System.out.println("Name: "+file.getPlayerName());*/
        
        //System.out.println(file.quickFind(TLPlayerSave.PET_HASH));
        //System.out.println(file.quickFind(TLPlayerSave.PET_HASH));
        
        //file.readPetInfo();
        //System.out.println(file.getPetName());
        //System.out.println(file.getPetType());
        
        //now find items...
        //search for DELIMs, if it is immediately after SEP then we've reached the end of the inventory
        
        //D . . . x x N A M E . . . D
        //Delim, 7 bytes, strlen, name, 6 bytes, delim
        /*
        byte prev = NIL;
        success = file.nextByte(DE, true);
        while (success && (prev=file.previousByte()) != FF) {
            //if DELIM proceeds anything other than NIL, it is probably not an item
            if (prev == NIL) {
                //get item name .. we use 8 because seekBack is true above
                file.step(8);
                String itemName = file.readTLString();
                
                //skip past the next DE
                file.nextByte(DE, false);
                
                //step to the item location
                file.step(40);
                int itemLoc = file.readTLInt();
                String locStr;
                if (itemLoc==0)
                    locStr = "EQUIPPED";
                else if (itemLoc==-1)
                    locStr = "SOCKETED";
                else
                    locStr = String.valueOf(itemLoc);
                System.out.println(itemName+" ["+locStr+"] at pointer "+file.getFilePointer());
            } else { //let's keep looking
                file.step();
            }
            
            //start over again, seeking to the start of the next delim
            success = file.nextByte(DE, true);
        }*/
    }
    
    
    //TODO: file save replaceTLString SharedStash DOES NOT USE EOF INT
    /** 
     * Replaces the TLString at start position with another string (which may be
     * a different length). The TLSave stream will be closed after calling this.
     * 
     * @param save
     * @param start
     * @param str
     * @return -1 unsuccessful, 0 if successful, 1 if successful and closed
     * @throws java.io.IOException
     */
    public static int replaceTLString(TLSave save, String str) throws IOException {
        int ret = 0;
        long start = save.getFilePointer();
        short oldStrlen = save.readTLShort();
        short newStrlen = (short)str.length();
        
        //back up so we can overwrite the strlen short we just read
        save.step(-2);
        
        //determine new length
        long oldLength = save.length();
        long newLength = oldLength + (newStrlen - oldStrlen) * 2;
        
        File saveFile = save.getFile();
        
        //if we aren't inserting or removing any bytes
        if (newLength == oldLength) {
            save.writeTLString(str);
        } else {
            if (newLength > Integer.MAX_VALUE)
                throw new IOException("long file length unsupported");
            if (start > Integer.MAX_VALUE)
                throw new IOException("long start value unsupported");
            
            //open a new file for writing
            File temp = new File(saveFile.getPath()+".tmp");
            FileOutputStream out = new FileOutputStream(temp);
            
            //update the file length before we start writing data
            save.seek(save.length()-4);
            save.writeTLInt((int)newLength);
            
            //read the first part, it may be long so we'll use chunks of 1024 to
            //save memory
            save.seek(0);
            byte[] data = new byte[1024];
            int len = 0;
            int startLen = (int)start;
            int readLen = Math.min(data.length, startLen);
            while ((len = save.read(data, 0, readLen)) > 0) {
                out.write(data, 0, len);
                startLen -= len;
                readLen = Math.min(data.length, startLen);
            }
            
            //now read the first chunk up till the strlen short of the player name
            /*byte[] data = new byte[(int)start];
            save.seek(0);
            save.readFully(data);
            //writes the first chunk of data
            out.write(data);*/            
            
            //write the new string length
            short strlen = Short.reverseBytes(newStrlen);
            out.write((strlen >>> 8) & 0xFF);
            out.write((strlen >>> 0) & 0xFF);
            //write the new string
            data = str.getBytes(ENCODING);
            out.write(data);
            
            //in the save file, step ahead past the old name
            save.step(2+oldStrlen*2);
            
            //now we just write the rest of the save file to the temp
            data = new byte[1024];
            len = 0;
            while ((len = save.read(data)) > 0) {
                out.write(data, 0, len);
            }
            
            //and close both files
            save.close();
            out.close();
            
            //lastly, we replace the original with the temp...
            saveFile.delete();
            boolean renamed = temp.renameTo(saveFile);
            ret = renamed ? 1 : -1;
        }
        return ret;
    }
        
    private File file;
    
    //Method to safely insert a string in the middle of a TL .SVT
    //e.g. safely changing a character name and its length
    // overwriteTLStringSafe(String str)
    //    assumes that the pointer is sitting before a TLString
    //REALLY DIFFICULT unless there is a clear pattern after the name
    //game seems to recognize the change
    
    //however we can still do simple name changes (i.e. less than or equal to,
    //using spaces)
    //for now though, focus on gold transfering and item reading
    
    public TLSave(String file, String mode) throws IOException {
        this(new File(file), mode);
    }
    
    public TLSave(File file, String mode) throws IOException {
        super(file, mode);
        this.file = file;
    }
    
    public File getFile() {
        return file;
    }
    
    public void writeTLShort(short s) throws IOException {
        writeShort(Short.reverseBytes(s));
    }
    
    public void writeTLInt(int i) throws IOException {
        writeInt(Integer.reverseBytes(i));
    }
        
    /** Reads a TL int, i.e. reads an int with reversed bytes. */
    public int readTLInt() throws IOException {
        return Integer.reverseBytes(readInt());
    }
    
    /** Reads a TL short, i.e. reads a shrot with reversed bytes. */
    public short readTLShort() throws IOException {
        return Short.reverseBytes(readShort());
    }
        
    /** 
     * Reads a TL string. The pointer should be placed 
     * before the strlen descriptor (a short).
     */
    public String readTLString() throws IOException {
        short len = readTLShort();
        long p = getFilePointer();
        
        byte[] b = new byte[len*2]; //allow for NIL bytes
        read(b, 0, b.length);
        return new String(b, ENCODING);
    }
    
    public void writeTLString(String str) throws IOException {
        short len = (short)str.length();
        writeTLShort(len);
        byte[] b = str.getBytes(ENCODING);
        write(b);
    }
    
    public void step() throws IOException {
        step(1);
    }
    
    public void step(long i) throws IOException {
        seek(getFilePointer()+i);
    }
    
    public boolean nextByte(byte b) throws IOException {
        return nextByteGroup(b, 1, false, true);
    }
    
    public boolean nextByte(byte b, boolean seekBack) throws IOException {
        return nextByteGroup(b, 1, false, seekBack);
    }
    
    /** Returns the previous byte. */
    public byte previousByte() throws IOException {
        step(-1);
        return readByte();
    }
    
    public boolean nextByteGroup(byte b, int x, boolean exact) throws IOException {
        return nextByteGroup(b, x, exact, true);
    }
    
    /**
     * From the current filePointer, seeks forward until exactly 'x' number of
     * bytes 'b' are found together. False is returned if the EOF was reached
     * before finding any groups (the pointer is reset to it's old value).
     * If exact is true, only looks for groups with exactly X number of bytes 'b'. 
     * Otherwise it will look for X or more number of bytes 'b'. 
     * If seekBack is true, the pointer will be placed directly before the byte 
     * group, otherwise it will not be backed up.
     * 
     * Note: looking for byte 0x01 might be buggy
     */
    public boolean nextByteGroup(byte b, int x, boolean exact, 
            boolean seekBack) throws IOException {
        if (x==0)
            throw new IllegalArgumentException("x can't be 0");        
        long start = getFilePointer();
        //search through the rest of the file
        int count = 0;
        long groupStart = 0;
        boolean skipGroup = false;
        
        while (getFilePointer() < length()) {
            //get the next byte
            byte next = readByte();
            //is that byte what we are looking for?
            //if we are skipping a group, ignore
            if (next == b && !skipGroup) {
                //it's the start of a group
                if (count==0)
                    groupStart = getFilePointer()-1;
                count++;
                //if we have found exactly X of them
                if (count == x) {
                    //don't search for exactly X, so it doesn't matter what's next
                    //just move & return
                    if (!exact) {
                        if (seekBack)
                            seek(groupStart);
                        return true;
                    }
                    //otherwise.. if it is exact
                    //and it's not the EOF, let's lookahead
                    else if (getFilePointer() < length()) {
                        byte lookahead = readByte();
                        //yup, we had found the end of a group
                        //reset the pointer to the start of the group
                        if (lookahead != b) {
                            if (seekBack)
                                seek(groupStart);
                            else
                                step(-1);
                            return true;
                        } else {
                            //it's a group, but not the right size.. ignore it
                            skipGroup = true;
                            step(-1);
                        }
                    } 
                    //we've reached the EOF with exactly X.. it's a group
                    else {
                        if (seekBack)
                            seek(groupStart);
                        return true;
                    }
                    
                }
            } else if (next != b) {
                //it's not the byte we are looking for... reset everything
                skipGroup = false;
                count = 0;
                groupStart = 0;
            }
        }
        if (seekBack)
            seek(start);
        return false;
    }
}
